﻿using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using Esponce.Common;
#if SILVERLIGHT
using System.Threading;
using System.Windows;
using System.Net.Browser;
#elif WINRT
using Windows.UI;
using System.Threading.Tasks;
#endif

namespace Esponce.QRCode
{
	public abstract class QRCodeClientBase
	{
		#region Constructor
		public QRCodeClientBase()
		{
			//Default values
			this.Format = ApiFormat.Json;
			this.UseSSL = true;
		}
		#endregion

		#region Public properties
		/// <summary>
		/// Gets the base URL for API calls.
		/// </summary>
		public string BaseUrl
		{
			get
			{
				string scheme = this.UseSSL ? "https" : "http";
				return scheme + "://www.esponce.com/api/v3/";
			}
		}

		/// <summary>
		/// Gets or sets a serializable format type for data transfer.
		/// </summary>
		public ApiFormat Format
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets API key. The key identifies and authorizes user.
		/// </summary>
		public string ApiKey
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets a value indicating whether to use secure line.
		/// </summary>
		public bool UseSSL
		{
			get;
			set;
		}

		/// <summary>
		/// Gets a User-Agent for HTTP request header.
		/// </summary>
		public string UserAgent
		{
			get
			{
#if WINRT
				//TODO: Get "Windows 8" dynamically, Enivornment.OSVersion is no longer available in WinRT
				string ua = string.Format("QRCode/3.0 ({0}; WinRT; {1})", "Windows 8", CultureInfo.CurrentCulture.TextInfo.CultureName);
#else			
				string ua = string.Format("QRCode/3.0 ({0}; .NET CLR {1}; {2})", Environment.OSVersion, Environment.Version, CultureInfo.CurrentCulture.TextInfo.CultureName);
#endif
				if (!string.IsNullOrEmpty(this.ApplicationName))
				{
					ua += " " + this.ApplicationName;
				}
				return ua;
			}
		}

		/// <summary>
		/// Gets or sets the application name that is using the API. Value is optional and appears in UserAgent for identifying source or for solving potential problems.
		/// </summary>
		public string ApplicationName
		{
			get;
			set;
		}

		/// <summary>
		/// Gets a helper for building URIs.
		/// </summary>
		public QRCodeUriBuilder UriBuilder
		{
			get
			{
				var result = new QRCodeUriBuilder();
				result.BaseUrl = this.BaseUrl;
				result.ApiKey = this.ApiKey;
				return result;
			}
		}

		/// <summary>
		/// Gets a number of available API calls that can be made until the rate limit counter resets. Value applies to the last called method.
		/// </summary>
		public int? RateLimitRemaining
		{
			get;
			protected set;
		}

		/// <summary>
		/// Gets the total number of available requests per API call for a specific method. Value applies to the last called method.
		/// </summary>
		public int? RateLimitTotal
		{
			get;
			protected set;
		}

		/// <summary>
		/// Gets duration when the rate limit counter will reset. Value in milliseconds and applies to the last called method.
		/// </summary>
		public int? RateLimitReset
		{
			get;
			protected set;
		}

		/// <summary>
		/// Gets or sets a potential warning message of last request.
		/// </summary>
		public string LastWarning
		{
			get;
			set;
		}
		#endregion

		#region Protected methods
		/// <summary>
		/// Gets a mime type according to the selected data format.
		/// </summary>
		/// <returns>Returns data format mime type.</returns>
		protected string GetMimeType()
		{
			switch (this.Format)
			{
				case ApiFormat.Json:
					return "application/json";

				case ApiFormat.Xml:
					return "application/xml";

				default:
					throw new NotSupportedException("Format '" + this.Format + "' is not supported!");
			}
		}

		/// <summary>
		/// Converts an object to string according to the selected data format.
		/// </summary>
		/// <param name="model">Object to be serialized.</param>
		/// <returns>Returns a serialized model as string.</returns>
		protected string Serialize(object model)
		{
			switch (this.Format)
			{
				case ApiFormat.Json:
					return Helper.JsonSerialize(model);

				case ApiFormat.Xml:
					return Helper.XmlSerialize(model);

				default:
					throw new NotSupportedException("Format '" + this.Format + "' is not supported!");
			}
		}

		/// <summary>
		/// Converts a string back to object according to the selected data format.
		/// </summary>
		/// <typeparam name="T">Type of expected output object.</typeparam>
		/// <param name="content">Content to be deserialized.</param>
		/// <returns>Returns a deserialized object.</returns>
		protected T Deserialize<T>(string content)
		{
			switch (this.Format)
			{
				case ApiFormat.Json:
					return Helper.JsonDeserialize<T>(content);

				case ApiFormat.Xml:
					return Helper.XmlDeserialize<T>(content);

				default:
					throw new NotSupportedException("Format '" + this.Format + "' is not supported!");
			}
		}

		/// <summary>
		/// Converts a string back to object according to the selected data format.
		/// </summary>
		/// <typeparam name="T">Type of expected output object.</typeparam>
		/// <param name="stream">Stream to be deserialized.</param>
		/// <returns>Returns a deserialized object.</returns>
		protected T Deserialize<T>(Stream stream) where T : class
		{
			using (var reader = new StreamReader(stream))
			{
				string raw = reader.ReadToEnd();
				T result = (raw != null ? Deserialize<T>(raw) : null);
				return result;
			}
		}

		/// <summary>
		/// Collects rate limit values from a HTTP response header.
		/// </summary>
		/// <param name="response">HTTP response object.</param>
		protected void CollectRateLimit(HttpWebResponse response)
		{
			int total;
			if (int.TryParse(response.Headers["X-RateLimit-Limit"], out total))
			{
				this.RateLimitTotal = total;
			}
			else
			{
				this.RateLimitTotal = null;
			}

			int remaining;
			if (int.TryParse(response.Headers["X-RateLimit-Remaining"], out remaining))
			{
				this.RateLimitRemaining = remaining;
			}
			else
			{
				this.RateLimitRemaining = null;
			}

			int reset;
			if (int.TryParse(response.Headers["X-RateLimit-Reset"], out reset))
			{
				this.RateLimitReset = reset;
			}
			else
			{
				this.RateLimitReset = null;
			}
		}

#if SILVERLIGHT
		/// <summary>
		/// Makes a HTTP request and gets response stream.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="input">Content to be sent in request body. Only for POST and PUT methods.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected Stream Request(string verb, string url, Stream input, string accept = null)
		{
			this.LastWarning = null;

			Stream output = null;
			Exception error = null;

			var mre = new ManualResetEvent(false);
			mre.Reset();

			RequestAsync(verb, url, input, (ex, stream) =>
			{
				error = ex;
				output = stream;
				mre.Set();
			});

			//Block until the async callback completes
			mre.WaitOne();

			if (error != null)
			{
				throw error;
			}

			return output;
		}

		/// <summary>
		/// Makes a HTTP request and gets response stream.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="model">Object to be serialized and sent in request body. Only for POST and PUT methods.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected Stream Request(string verb, string url, object model = null, string accept = null)
		{
			Stream stream = null;
			if (model != null)
			{
				string content = Serialize(model);
				stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
			}

			return Request(verb, url, stream, accept);
		}
#else

#if WINRT
		/// <summary>
		/// Makes a HTTP request and gets response stream.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="input">Content to be sent in request body. Only for POST and PUT methods.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected Stream Request(string verb, string url, Stream input, string accept = null)
		{
			this.LastWarning = null;

			//Prepare absolute URI
			Uri uri = new Uri(url, UriKind.Absolute);

			//Prepare for a HTTP request
			var request = HttpWebRequest.Create(uri) as HttpWebRequest;
			//request.Headers["User-Agent"] = this.UserAgent;
			request.Accept = accept ?? GetMimeType();
			request.Method = verb;

			//Make async method sync again
			var mre = new System.Threading.ManualResetEvent(false);

			//Fill request stream
			if (input != null && (verb == "POST" || verb == "PUT"))
			{
				const int size = 1024;

				request.BeginGetRequestStream(new AsyncCallback(iar =>
				{
					using (var stream = request.EndGetRequestStream(iar))
					{
						int length = 0;
						byte[] buffer = new byte[size];
						while ((length = input.Read(buffer, 0, size)) != 0)
						{
							stream.Write(buffer, 0, length);
						}
					}

					mre.Set();
				}), null);

				mre.Reset();
				mre.WaitOne();
			}

			Stream output = null;
			Exception error = null;

			//Make a request to get response
			request.BeginGetResponse(new AsyncCallback(iar =>
			{
				HttpWebResponse response = null;

				try
				{
					response = request.EndGetResponse(iar) as HttpWebResponse;
				}
				catch (Exception ex)
				{
					var w = ex as WebException;
					if (w != null)
					{
						response = w.Response as HttpWebResponse;
					}

					if (response == null)
					{
						error = ex;
					}
				}

				if (response != null)
				{
					this.LastWarning = response.Headers["Warning"];

					//Check for errors
					int code = 0;
					string xapierror = response.Headers["X-Api-Error"];
					string xapicode = response.Headers["X-Api-Error-Code"];
					if (xapierror != null && int.TryParse(xapicode, out code))
					{
						error = new QRCodeException(code, xapierror)
						{
							StatusCode = response.StatusCode,
							StatusDescription = response.StatusDescription
						};
					}

					//Get rate limit info from headers
					CollectRateLimit(response);

					output = response.GetResponseStream();
				}

				mre.Set();
			}), null);

			mre.Reset();
			mre.WaitOne();

			if (error != null)
			{
				throw error;
			}

			return output;
		}
#else
		/// <summary>
		/// Makes a HTTP request and gets response stream.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="input">Content to be sent in request body. Only for POST and PUT methods.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected Stream Request(string verb, string url, Stream input, string accept = null)
		{
			this.LastWarning = null;

			//Prepare absolute URI
			Uri uri = new Uri(url, UriKind.Absolute);

			//Prepare for a HTTP request
			var request = HttpWebRequest.Create(uri) as HttpWebRequest;
#if WINRT
			//request.Headers["User-Agent"] = this.UserAgent;
#else
			request.UserAgent = this.UserAgent;
#endif
			request.Accept = accept ?? GetMimeType();
			request.Method = verb;

			//Fill request stream
			if (input != null && (verb == "POST" || verb == "PUT"))
			{
				const int size = 1024;
				using (var stream = request.GetRequestStream())
				{
					int length = 0;
					byte[] buffer = new byte[size];
					while ((length = input.Read(buffer, 0, size)) != 0)
					{
						stream.Write(buffer, 0, length);
					}
				}
			}

			//Make a request to get response
			var response = request.GetResponse() as HttpWebResponse;

			this.LastWarning = response.Headers["Warning"];

			//Check for errors
			int code = 0;
			string xapierror = response.Headers["X-Api-Error"];
			string xapicode = response.Headers["X-Api-Error-Code"];
			if (xapierror != null && int.TryParse(xapicode, out code))
			{
				throw new QRCodeException(code, xapierror)
				{
					StatusCode = response.StatusCode,
					StatusDescription = response.StatusDescription
				};
			}

			//Get rate limit info from headers
			CollectRateLimit(response);

			var output = response.GetResponseStream();
			return output;
		}
#endif

		/// <summary>
		/// Makes a HTTP request and gets response stream.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="model">Object to be serialized and sent in request body. Only for POST and PUT methods.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected Stream Request(string verb, string url, object model = null, string accept = null)
		{
			Stream stream = null;
			if (model != null)
			{
				string content = Serialize(model);
				stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
			}

			return Request(verb, url, stream, accept);
		}
#endif

		/// <summary>
		/// Makes a HTTP request asynchronously. Set callback to get response.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="input">Content to be sent in request body. Only for POST and PUT methods.</param>
		/// <param name="callback">Callback for response. Always check for the exception first then use the response stream.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected void RequestAsync(string verb, string url, Stream input, Action<Exception, Stream> callback, string accept = null)
		{
			RequestAsync(verb, url, input, (error, stream, length) =>
			{
				callback(error, stream);
			}, accept);
		}

		/// <summary>
		/// Makes a HTTP request asynchronously. Set callback to get response.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="input">Content to be sent in request body. Only for POST and PUT methods.</param>
		/// <param name="callback">Callback for response. Always check for the exception first then use the response stream.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected void RequestAsync(string verb, string url, Stream input, Action<Exception, Stream, long> callback, string accept = null)
		{
			this.LastWarning = null;
			Uri uri = new Uri(url, UriKind.Absolute);

#if SILVERLIGHT
			//var request = WebRequestCreator.BrowserHttp.Create(uri) as HttpWebRequest;
			var request = HttpWebRequest.CreateHttp(uri) as HttpWebRequest;

			//request.Headers["User-Agent"] = this.UserAgent; //NOTE: This is not possible in Silverlight, throws exception when trying to overwrite User-Agent header
			request.Headers["X-User-Agent"] = this.UserAgent;
#elif WINRT
			var request = HttpWebRequest.Create(uri) as HttpWebRequest;
			//request.Headers["User-Agent"] = this.UserAgent;
#else
			var request = HttpWebRequest.Create(uri) as HttpWebRequest;
			request.UserAgent = this.UserAgent;
#endif

			request.Accept = accept ?? GetMimeType();
			request.Method = verb;

			//Callback to handle response
			var responseCallback = new AsyncCallback(result =>
			{
				Exception error = null;
				Stream stream = null;
				long length = 0;

				try
				{
					var req = result.AsyncState as HttpWebRequest;
					var response = req.EndGetResponse(result) as HttpWebResponse;

					this.LastWarning = response.Headers["Warning"];

					//Check for errors
					int code = 0;
					string xapierror = response.Headers["X-Api-Error"];
					string xapicode = response.Headers["X-Api-Error-Code"];
					string xapihint = response.Headers["X-Api-Error-Hint"];
					if (xapierror != null && int.TryParse(xapicode, out code))
					{
						throw new QRCodeException(code, xapierror, xapihint)
						{
							StatusCode = response.StatusCode,
							StatusDescription = response.StatusDescription
						};
					}

					//Get rate limit info from headers
					CollectRateLimit(response);

					length = response.ContentLength;
					stream = response.GetResponseStream();
				}
				catch (Exception ex)
				{
					error = ex;
				}
				finally
				{
					if (callback != null)
					{
						callback(error, stream, length);
					}
				}
			});

			if (input != null && (verb == "POST" || verb == "PUT"))
			{
				request.BeginGetRequestStream(new AsyncCallback(result =>
				{
					try
					{
						//Fill the request stream
						using (var stream = request.EndGetRequestStream(result))
						{
							int length = 0;
							const int size = 1024;
							byte[] buffer = new byte[size];
							while ((length = input.Read(buffer, 0, size)) != 0)
							{
								stream.Write(buffer, 0, length);
							}
						}

						//Get the response
						request.BeginGetResponse(responseCallback, request);
					}
					catch (Exception ex)
					{
						if (callback != null)
						{
							callback(ex, null, 0);
						}
					}
				}), request);
			}
			else
			{
				//Make a request to get response
				request.BeginGetResponse(responseCallback, request);
			}
		}

		/// <summary>
		/// Makes a HTTP request asynchronously. Set callback to get response.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="model">Object to be serialized and sent in request body. Only for POST and PUT methods.</param>
		/// <param name="callback">Callback for response. Always check for the exception first then use the response stream.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected void RequestAsync(string verb, string url, object model, Action<Exception, Stream> callback, string accept = null)
		{
			RequestAsync(verb, url, model, (error, stream, length) =>
			{
				callback(error, stream);
			}, accept);
		}

		/// <summary>
		/// Makes a HTTP request asynchronously. Set callback to get response.
		/// </summary>
		/// <param name="verb">HTTP method: GET, POST, etc.</param>
		/// <param name="url">URL to make a request to.</param>
		/// <param name="model">Object to be serialized and sent in request body. Only for POST and PUT methods.</param>
		/// <param name="callback">Callback for response. Always check for the exception first then use the response stream.</param>
		/// <param name="accept">Value for "Accept" in HTTP request header, e.g. "image/png,application/json"</param>
		/// <returns>Returns response stream.</returns>
		protected void RequestAsync(string verb, string url, object model, Action<Exception, Stream, long> callback, string accept = null)
		{
			Stream stream = null;
			if (model != null)
			{
				string content = Serialize(model);
				stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
			}

			RequestAsync(verb, url, stream, callback, accept);
		}
		#endregion
	}
}
